package org.sylfra.idea.plugins.xstructure.resolution.impl.defaultt;

import com.intellij.openapi.components.ApplicationComponent;
import com.intellij.psi.xml.XmlAttribute;
import com.intellij.psi.xml.XmlFile;
import com.intellij.psi.xml.XmlTag;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.sylfra.idea.plugins.xstructure.XStructurePlugin;
import org.sylfra.idea.plugins.xstructure.config.IXMapping;
import org.sylfra.idea.plugins.xstructure.config.IXMappingExp;
import org.sylfra.idea.plugins.xstructure.config.IXMappingSet;
import org.sylfra.idea.plugins.xstructure.resolution.IXMappingResolver;
import org.sylfra.idea.plugins.xstructure.resolution.ImplNotSupportedException;
import org.sylfra.idea.plugins.xstructure.resolution.XMappingException;

import java.util.regex.Pattern;

/**
 * Implementation of resolver for 'default' implementation type
 *
 * @author <a href="mailto:sylfradev@yahoo.fr">Sylvain FRANCOIS</a>
 * @version $Id: XMappingResolverDefaultImpl.java 31 2007-12-23 11:23:10Z syllant $
 */
public class XMappingResolverDefaultImpl implements IXMappingResolver, ApplicationComponent
{
  private static final String TYPE = "default";

  /**
   * {@inheritDoc}
   */
  @NonNls
  @NotNull
  public String getComponentName()
  {
    return XStructurePlugin.COMPONENT_NAME + ".XMappingResolverDefaultImpl";
  }

  /**
   * {@inheritDoc}
   */
  public void initComponent()
  {
    XStructurePlugin.getInstance().getXMappingResolverFactory().register(this);
  }

  /**
   * {@inheritDoc}
   */
  public void disposeComponent()
  {
  }

  /**
   * {@inheritDoc}
   */
  public String getType()
  {
    return TYPE;
  }

  /**
   * {@inheritDoc}
   */
  @NotNull
  public IXMappingSet createMappingSet()
  {
    return new XMappingSetDefaultImpl(this);
  }

  /**
   * {@inheritDoc}
   */
  @NotNull
  public IXMapping createMapping()
  {
    return new XMappingDefaultImpl();
  }

  /**
   * {@inheritDoc}
   */
  public void validateMapping(@NotNull IXMapping xMapping) throws XMappingException
  {
    XMappingDefaultImpl regexpXMapping = (XMappingDefaultImpl) xMapping;
    Pattern pattern;
    try
    {
      pattern = Pattern.compile(xMapping.getMatchString());
    }
    catch (Exception e)
    {
      throw new XMappingException("Failed to compile regexp : " + xMapping.getMatchString());
    }
    regexpXMapping.setMatchPattern(pattern);
  }

  /**
   * {@inheritDoc}
   */
  public void checkSupported() throws ImplNotSupportedException
  {
    // Always supported
  }

  /**
   * {@inheritDoc}
   */
  @NotNull
  public String resolveTargetText(@NotNull XmlTag xmlTag, @NotNull IXMappingExp mappingExp)
    throws XMappingException
  {
    StringBuffer result = new StringBuffer();

    XMappingExpDefaultImpl regexpMappingExp = (XMappingExpDefaultImpl) mappingExp;
    String rawExp = regexpMappingExp.getRawExp();

    // Parse multi exp : xxx{PATH}xxx{PATH}...
    int startGroup;
    int endGroup;

    while (rawExp.length() > 0)
    {
      startGroup = rawExp.indexOf('{');

      // Raw exp ends with "{.". These chars are interpeted as raw text
      if ((startGroup == -1) || (startGroup >= rawExp.length() - 2))
      {
        result.append(rawExp);
        break;
      }

      // Previous raw text
      if (startGroup > 0)
      {
        result.append(rawExp.substring(0, startGroup));
        rawExp = rawExp.substring(startGroup);
      }

      // Localize next group
      endGroup = rawExp.indexOf('}');
      if (endGroup == -1)
      {
        result.append(rawExp);
        break;
      }

      result.append(internalResolveSingleExp(xmlTag, rawExp.substring(1, endGroup)));

      rawExp = (endGroup == rawExp.length() - 1) ? "" : rawExp.substring(endGroup + 1);
    }

    return result.toString();
  }

  /**
   * {@inheritDoc}
   */
  public IXMapping findMatchingMapping(@NotNull IXMappingSet xMappingSet, @NotNull XmlTag xmlTag)
  {
    String path = buildXmlTagPath(xmlTag);
    for (IXMapping xMapping : xMappingSet.getMappings())
    {
      XMappingDefaultImpl regexpXMapping = (XMappingDefaultImpl) xMapping;

      if (regexpXMapping.getMatchPattern().matcher(path).matches())
      {
        return xMapping;
      }
    }
    return null;
  }

  private String internalResolveSingleExp(XmlTag xmlTag, String rawExp)
    throws XMappingException
  {
    if (rawExp.startsWith("/"))
    {
      xmlTag = ((XmlFile) xmlTag.getContainingFile()).getDocument().getRootTag();
      rawExp = rawExp.substring(1);
    }

    return internalResolveSingleExpPart(xmlTag, rawExp);
  }

  private String internalResolveSingleExpPart(XmlTag xmlTag, String mappingExp)
    throws XMappingException
  {
    int startNode = mappingExp.indexOf('/');
    int startAtt = mappingExp.indexOf('@');

    if ((startAtt > -1) && (startAtt < startNode))
    {
      throw new XMappingException("Bad expression, attribute may only be declared on last node : "
        + mappingExp);
    }

    // Related to this tag
    // Text or attribute ?
    if (startNode == -1)
    {
      if (startAtt == 0)
      {
        mappingExp = mappingExp.substring(1);
        XmlAttribute attribute = xmlTag.getAttribute(mappingExp);
        if (attribute == null)
        {
          return "";
        }

        return attribute.getValue();
      }

      if (startAtt > 0)
      {
        throw new XMappingException("Bad expression, attribute should be declared as @att : "
          + mappingExp);
      }
    }

    // This tag text ?
    if ((".".equals(mappingExp)) || ("".equals(mappingExp)))
    {
      return xmlTag.getValue().getText();
    }

    // Loooking for matching child
    String nextTagName = (startNode == -1) ? mappingExp : mappingExp.substring(0, startNode);
    for (XmlTag childTag : xmlTag.getSubTags())
    {
      if (mappingExp.equals(childTag.getName()))
      {
        return childTag.getValue().getText();
      }
      if (nextTagName.equals(childTag.getName()))
      {
        return internalResolveSingleExpPart(childTag, mappingExp.substring(startNode + 1));
      }
    }

    return "";
  }

  /**
   * Build a path from parent hierarchy
   *
   * @param xmlTag the request XML tag
   *
   * @return a path which identifies this tag
   */
  private String buildXmlTagPath(XmlTag xmlTag)
  {
    StringBuffer result = new StringBuffer();

    while (xmlTag != null)
    {
      result.insert(0, xmlTag.getName());
      result.insert(0, '/');

      xmlTag = xmlTag.getParentTag();
    }
    return result.toString();
  }
}